﻿using System.Linq;
using System.Xml;
using System.Xml.XPath;
using XmlStudio.Enums;
using XmlStudio.Interfaces;
using XmlStudio.Utilities;
using System.Threading;
using System.ComponentModel;
using System.IO;
using System.Collections.Generic;

namespace XmlStudio.DataLayer {

    /// <summary>
    /// Internal implementation of node.
    /// </summary>
    internal class BigXmlNodeInternal : BigXmlNode {
        public static int NodeCount;
        public static int Total;

        /// <summary>
        /// Initializes a new instance of the <see cref="BigXmlNodeInternal"/> class.
        /// </summary>
        /// <param name="node"><see cref="XmlNode"/> to set.</param>
        /// <param name="parent">Node's parent.</param>
        /// <param name="address">Address of the node.</param>
        /// <param name="originalAddress">Original address of the node.</param>
        private BigXmlNodeInternal(XmlNode node, BigXmlNode parent, int[] address, int[] originalAddress) {
            this.xmlNode = node;
            this.parent = parent;
            this.address = address;
            this.originalAddress = originalAddress;

            this.UpdateAttributes();
        }

        /// <summary>
        /// Performs deep copy of the node.
        /// </summary>
        /// <returns></returns>
        public BigXmlNodeInternal DeepCopy() {
            //var node = new BigXmlNodeInternal(this.xmlNode.CloneNode(true)
            var childNodes = (from BigXmlNodeInternal child in this.ChildNodes where child != null select child.DeepCopy()).ToList();

            var node = new BigXmlNodeInternal(this.xmlNode.CloneNode(false), null, this.address.DeepClone(), this.originalAddress.DeepClone());
            node.ChildNodes.Clear();
            foreach(var c in childNodes.Where(c => c != null)) {
                node.ChildNodes.Add(c);
                node.xmlNode.AppendChild(c.xmlNode);
                c.SetParent(node);
            }

            return node;
        }

        /// <summary>
        /// Updates simpleTypes from underlying <see cref="XmlNode"/>.
        /// </summary>
        private void UpdateAttributes() {
            if(this.xmlNode != null) {
                this.attributes.Clear();

                var navigator = this.xmlNode.CreateNavigator();
                if(navigator != null) {
                    if(navigator.MoveToFirstAttribute()) {
                        do {
                            this.attributes.Add(BigXmlAttribute.CreateKey(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI),
                                                new BigXmlAttribute(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI, navigator.Value));
                        } while(navigator.MoveToNextAttribute());
                    }
                }
            }
        }

        /// <summary>
        /// Writed the node to the specified <see cref="XmlWriter"/>.
        /// </summary>
        /// <param name="writer"><see cref="XmlWriter"/> to use.</param>
        /// <param name="model">The model.</param>
        /// <param name="updateAddresses">Indicates whether to update addresses of those nodes which were moved hence their actual address is different from the original one.</param>
        /// <param name="report">The report.</param>
        public void Save(XmlWriter writer, IXmlModelWithNotifications model, bool updateAddresses, BackgroundWorker report) {
            this.Save(writer, model.Reader, updateAddresses, report);
        }

        /// <summary>
        /// Saved the node to the specified <see cref="XmlWriter"/>.
        /// </summary>
        /// <param name="writer"><see cref="XmlWriter"/> to use.</param>
        /// <param name="document">Reference to <see cref="IBigXmlDocument"/> which has been used to load this node.</param>
        /// <param name="updateAddresses">Indicates whether to update addresses of those nodes which were moved hence their actual address is different from the original one.</param>
        /// <param name="report">Background worker.</param>
        public void Save(XmlWriter writer, IBigXmlDocument document, bool updateAddresses, BackgroundWorker report) {
            this.Save(writer, document.Reader, updateAddresses, report);
        }

        /// <summary>
        /// Saves the current node to the specified <see cref="XmlWriter"/>.
        /// </summary>
        /// <param name="writer"><see cref="XmlWriter"/> where to save the current node.</param>
        /// <param name="reader"><see cref="IBigXmlReader"/> used to read the edited file.</param>
        /// <param name="updateAddresses">Indicates whether to update addresses of those nodes which were moved hence their actual address is different from the original one.</param>
        /// <param name="report">Background worker.</param>
        public void Save(XmlWriter writer, IBigXmlReader reader, bool updateAddresses, BackgroundWorker report) {
            // steps:
            // write the start tag + eventual simpleTypes
            // if there are child elements, write them recursively, if not, try to get them from the original file (like expand, but the whole subtree)
            // if the node is not entirely expanded, load the remaining nodes from original file
            // write end tag
            var navigator = this.xmlNode.CreateNavigator();

            BigXmlNodeInternal.NodeCount++;
            if(report != null) {
                if(BigXmlNodeInternal.NodeCount % BigXmlReader.NodeStep == 0) {
                    var progress = ((double)BigXmlNodeInternal.NodeCount / (double)BigXmlNodeInternal.Total) * 100;
                    report.ReportProgress((int)progress);
                    Thread.Sleep(BigXmlReader.Sleep);
                }
            }

            if(navigator != null) {
                switch(navigator.NodeType) {
                    case XPathNodeType.Element:
                        var prefix = writer.LookupPrefix(navigator.NamespaceURI);

                        if(!string.IsNullOrEmpty(prefix) && prefix == navigator.Prefix) {
                            writer.WriteStartElement(navigator.LocalName, navigator.NamespaceURI);
                        } else {
                            writer.WriteStartElement(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI);
                        }

                        if(navigator.MoveToFirstAttribute()) {
                            do {
                                writer.WriteAttributeString(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI, navigator.Value);
                            } while(navigator.MoveToNextAttribute());

                            // navigator.MoveToRoot();
                            //foreach (XmlAttribute att in navigator.Attributes)
                            //{
                            //    writer.WriteAttributeString(att.Prefix, att.LocalName, att.NamespaceURI, att.Value);
                            //}
                        }

                        if(this.ChildNodes.Count > 0 && this.NodeState == ExpandState.Expanded) {
                            foreach(var child in this.childNodes) {
                                ((BigXmlNodeInternal)child).Save(writer, reader, updateAddresses, report);
                            }
                        } else if(this.NodeState == ExpandState.Collapsed && this.OriginalAddress != null) {
                            reader.WriteChildNodesFromAddress(this.OriginalAddress, writer, report);
                        } else if(this.NodeState == ExpandState.PartiallyExpanded) {
                            // to do:
                            // get the rest of the child nodes & write it
                            var expandedRange = this.ExpandedRange;
                            var allExpanded = Enumerable.Range(1, this.TotalChildCount);

                            var toExpand = allExpanded.Except(expandedRange);
                            var addressToExpand = this.Address;
                            if(this.originalAddress != null) {
                                addressToExpand = this.originalAddress;
                            }

                            //for(int i = 0; i < this.TotalChildCount; ++i) {
                            //    if(this.ChildNodes[i] != null) {
                            //        var child = this.ChildNodes[i];
                            //        ((BigXmlNodeInternal)child).Save(writer, reader, updateAddresses, report);
                            //    } else {
                            //        var newAddress = addressToExpand.ToList();
                            //        newAddress.Add(i + 1);
                            //        reader.WriteChildNodesFromAddress(newAddress.ToArray(), writer, report, true);
                            //    }
                            //}

                            var rangeToWrite = new List<int>();
                            for(int i = 0; i < this.TotalChildCount; ++i) {
                                if(this.ChildNodes[i] == null) {
                                    rangeToWrite.Add(i+1);
                                } else {
                                    if(rangeToWrite.Count > 0) {
                                        reader.WriteChildNodesFromAddress(addressToExpand, writer, report, rangeToWrite, false);
                                        rangeToWrite.Clear();
                                    }

                                    var child = this.ChildNodes[i];
                                    ((BigXmlNodeInternal)child).Save(writer, reader, updateAddresses, report);
                                }
                            }

                            if(rangeToWrite.Count > 0) {
                                reader.WriteChildNodesFromAddress(addressToExpand, writer, report, rangeToWrite, false);
                                rangeToWrite.Clear();
                            }

                            //foreach(var i in addressToExpand) {
                            //    var newAddress = this.address.ToList();
                            //    newAddress.Add(i);
                            //    reader.WriteChildNodesFromAddress(newAddress.ToArray(), writer);
                            //}

                            //var childNodes = reader.GetChildNodes(addressToExpand, toExpand, this.TotalChildCount);
                            //var notNullChildNodes = from x in childNodes where x != null select x;
                            //foreach(var node in notNullChildNodes) {
                            //    this.ChildNodes[node.Address[node.Address.Length - 1] - 1] = node;
                            //}

                            //foreach(var child in this.ChildNodes) {
                            //    ((BigXmlNodeInternal)child).Save(writer, reader, updateAddresses, report);
                            //}
                        }

                        writer.WriteEndElement();
                        break;
                    case XPathNodeType.Text:
                        writer.WriteValue(navigator.Value);
                        break;
                }
            }

            if(updateAddresses) {
                // TODO: this is not ok, because validation causes this but actual document is not affected - should be now ok, not updating addresses when validating
                this.originalAddress = this.address.DeepClone();
                this.modified = false;
            }
        }

        /// <summary>
        /// Adds attribute to the collection of simpleTypes.
        /// </summary>
        /// <param name="attribute">Attribute to add.</param>
        public void AddAttribute(IBigXmlAttribute attribute) {
            this.attributes.Add(BigXmlAttribute.CreateKey(attribute.Prefix, attribute.LocalName, attribute.NamespaceURI), attribute);
        }

        /// <summary>
        /// Removes attribute from the collection of simpleTypes.
        /// </summary>
        /// <param name="name">Name of the attribute to remove.</param>
        /// <param name="namespaceURI">Namespace uri of the attribute to remove.</param>
        /// <param name="prefix">Prefix of the attribute to remove.</param>
        public void RemoveAttribute(string name, string namespaceURI, string prefix) {
            this.attributes.Remove(BigXmlAttribute.CreateKey(prefix, name, namespaceURI));
        }

        /// <summary>
        /// Sets the parent for the current node.
        /// </summary>
        /// <param name="node">Parent node.</param>
        public void SetParent(BigXmlNodeInternal node) {
            this.parent = node;
        }

        /// <summary>
        /// Sets the value indicating whether this node has been modified.
        /// </summary>
        /// <param name="value">Modified value.</param>
        public void SetModified(bool value) {
            this.modified = value;
        }

        /// <summary>
        /// Sets the address of the current node.
        /// </summary>
        /// <param name="address">Address to set.</param>
        public void SetAddress(int[] address) {
            this.address = address;
        }

        /// <summary>
        /// Gets or sets the <see cref="XmlNode"/> associated with the current node.
        /// </summary>
        public XmlNode XmlNode {
            get { return this.xmlNode; }
            set { this.xmlNode = value; }
        }

        /// <summary>
        /// Gets the left sibling of the specified child node.
        /// </summary>
        /// <param name="child">Child node of the current node which left sibling to get.</param>
        /// <returns>Reference to the left sibling of specified child node.</returns>
        public BigXmlNodeInternal GetLeftSibling(BigXmlNodeInternal child) {
            return this.GetPreviousSibling(child) as BigXmlNodeInternal;
        }

        /// <summary>
        /// Gets the right sibling of the specified child node.
        /// </summary>
        /// <param name="child">Child node of the current node which right sibling to get.</param>
        /// <returns>Reference to the right sibling of specified child node.</returns>
        public BigXmlNodeInternal GetRightSibling(BigXmlNodeInternal child) {
            return this.GetNextSibling(child) as BigXmlNodeInternal;
        }

        /// <summary>
        /// Creates a new instance of the node.
        /// </summary>
        /// <param name="node"><see cref="XmlNode"/> associated with the new node.</param>
        /// <param name="parent">Parent node of the new node.</param>
        /// <param name="address">Address of the new node.</param>
        /// <param name="originalAddress">Original address of the new node.</param>
        /// <returns>Reference to the created node.</returns>
        public static BigXmlNodeInternal Create(XmlNode node, BigXmlNodeInternal parent, int[] address, int[] originalAddress) {
            return new BigXmlNodeInternal(node, parent, address, originalAddress);
        }

        /// <summary>
        /// Creates a new instance of the node.
        /// </summary>
        /// <param name="node"><see cref="XmlNode"/> associated with the new node.</param>
        /// <returns>Reference to the created node.</returns>
        public static BigXmlNodeInternal Create(XmlNode node) {
            return new BigXmlNodeInternal(node, null, null, null);
        }

        /// <summary>
        /// Creates a new instance of the node.
        /// </summary>
        /// <param name="node"><see cref="XmlNode"/> associated with the node.</param>
        /// <param name="parent">Node's parent.</param>
        /// <returns>Reference to the created node.</returns>
        public static BigXmlNodeInternal Create(XmlNode node, BigXmlNodeInternal parent) {
            return new BigXmlNodeInternal(node, parent, null, null);
        }

        /// <summary>
        /// Creates a new instance of the node.
        /// </summary>
        /// <param name="node"><see cref="XmlNode"/> associated with the node.</param>
        /// <param name="address">Address of the new node.</param>
        /// <param name="originalAddress">Original address of the new node.</param>
        /// <returns>Reference to the created node.</returns>
        public static BigXmlNodeInternal Create(XmlNode node, int[] address, int[] originalAddress) {
            return new BigXmlNodeInternal(node, null, address, originalAddress);
        }

        /// <summary>
        /// Creates a new instance of the node.
        /// </summary>
        /// <param name="address">Address of the new node.</param>
        /// <returns>Reference to the created node.</returns>
        public static BigXmlNodeInternal Create(int[] address) {
            return new BigXmlNodeInternal(null, null, address, null);
        }
    }
}